﻿using System;
using System.IO;
using System.Linq;
using System.Text;
using ArtheaEngine;
using ArtheaServer.Commands;
using NLog;
using Thought.Net.Telnet;

namespace ArtheaServer
{
    public class Connection : IConnection
    {
        #region Flag Defines

        [Flag] public const long SuppressEcho = (1 << 1);
        [Flag] public const long SuppressGoAhead = (1 << 0);

        #endregion

        private static readonly Logger log = LogManager.GetCurrentClassLogger();
        private readonly TelnetClient client;
        private readonly StreamReader reader;
        private readonly ColorWriter writer;
        private Account account;
        private byte passwordCount;
        private StateHandler state;
        private StringBuilder pageBuf = new StringBuilder();

        public Connection(TelnetClient client)
        {
            this.client = client;
            client.WillReceived += TelnetWillRecieved;
            client.DoReceived += TelnetDoRecieved;
            client.DontReceived += TelnetDontRecieved;
            client.WontReceived += TelnetWontRecieved;
            client.SubnegotiationReceived += TelnetSubNegotiationRecieved;
            var stream = new TelnetStream(client);
            writer = new ColorWriter(stream, this) {AutoFlush = true};
            reader = new StreamReader(stream);
            ScreenHeight = 24;
            ScreenWidth = 80;
        }

        #region Telnet Handlers

        private void TelnetWillRecieved(object o, TelnetOptionEventArgs args)
        {
            log.Trace("Recieved Will " + args.Option);
            switch (args.Option)
            {
                case TelnetOption.WindowSize:
                    args.Agreed = true;
                    break;
                case TelnetOption.TerminalType:
                    args.Agreed = true;
                    client.SendSubnegotiation(TelnetOption.TerminalType);
                    break;
                case TelnetOption.SuppressGoAhead:
                    args.Agreed = true;
                    break;
                case TelnetOption.EndOfRecord:
                    args.Agreed = true;
                    break;
                case TelnetOption.Echo:
                    args.Agreed = true;
                    break;
            }
        }

        private void TelnetDoRecieved(object o, TelnetOptionEventArgs args)
        {
            log.Trace("Recieved Do " + args.Option);
            switch (args.Option)
            {
                case TelnetOption.Echo:
                    if (Flags.And(SuppressEcho))
                        args.Agreed = true;
                    break;
            }
        }

        private static void TelnetWontRecieved(object o, TelnetOptionEventArgs args)
        {
            log.Trace("Recieved Wont " + args.Option);
        }

        private static void TelnetDontRecieved(object o, TelnetOptionEventArgs args)
        {
            log.Trace("Recieved Dont " + args.Option);
        }

        private void TelnetSubNegotiationRecieved(object o, TelnetOptionEventArgs args)
        {
            switch (args.Option)
            {
                case TelnetOption.WindowSize:
                    ScreenHeight = BitConverter.ToInt16(args.Data.Reverse().ToArray(), 0);
                    ScreenWidth = BitConverter.ToInt16(args.Data.Reverse().ToArray(), 2);
                    log.Trace("Recieved " + args.Option + " " + ScreenWidth + " " + ScreenHeight);
                    break;
                case TelnetOption.TerminalType:
                    TerminalType = World.Encoding.GetString(args.Data.Skip(1).ToArray());
                    break;
            }
        }

        #endregion

        public string TerminalType { get; private set; }

        public long Flags { get; set; }

        #region IConnection Members

        public Player Playing { get; private set; }

        public short ScreenHeight { get; private set; }

        public short ScreenWidth { get; private set; }

        public bool HasColor()
        {
            return Playing != null && Playing.Flags.And(Player.Color);
        }

        public void WriteLine(string format, params object[] args)
        {
            writer.WriteLine(format, args);
        }

        public void Write(string value)
        {
            writer.Write(value);
        }

        public void Write(string format, params object[] args)
        {
            writer.Write(format, args);
        }

        public void Write(byte[] value)
        {
            client.Write(value, 0, value.Length);
        }

        public void Write(object obj)
        {
            writer.Write(obj);
        }

        public void WriteLine(string value)
        {
            writer.WriteLine(value);
        }

        public void WriteLine()
        {
            writer.WriteLine();
        }

        public void WriteLine(object obj)
        {
            writer.WriteLine(obj);
        }

        public bool HasPage()
        {
            return pageBuf.Length > 0;
        }

        public void Page(StringBuilder buf)
        {
            if (HasPage())
            {
                pageBuf.Append(buf);
            }
            else
            {
                pageBuf = buf;
                ShowPage(string.Empty);
            } 
        }

        public void ShowPage(string input)
        {
            if (input.Length > 0)
            {
                pageBuf.Length = 0;
                return;
            }

            var text = pageBuf.ToString();

            var length = text.CountTextInLines(ScreenHeight - 1);

            if (length > 0)
            {
                text = text.Substring(0, length);
                pageBuf.Remove(0, length);
            }
            else
            {
                pageBuf.Length = 0;
            }

            Write(text);
        }

        #endregion

        #region State Handlers

        private void PlayingState(string argument)
        {
            if (HasPage())
            {
                ShowPage(argument);
                return;
            }

            try
            {
                Command.Interpret(Playing, new Argument(argument));
                WriteLine();
                Write("> ");
            }
            catch (QuitCommandException)
            {
                Playing.Dispose();

                Close();
            }
            catch (Exception ex)
            {
                log.Error(ex.Message);
                log.Trace(ex.StackTrace);
            }
        }

        private void GetAccountName(string argument)
        {
            if (string.IsNullOrEmpty(argument))
            {
                Write("Please enter a valid account name: ");
                return;
            }

            foreach (Account a in World.DataContext.Accounts)
            {
                if (!a.Name.Equals(argument, StringComparison.CurrentCultureIgnoreCase)) continue;

                account = a;
                Write("Password: ");
                RequestEchoOff();
                SetState(GetAccountPassword);
                return;
            }

            account = new Account {Name = argument};
            Write("Would you like to create an account for " + argument + "? (Y/n)");
            SetState(ConfirmNewAccount);
        }

        /// <summary>
        /// Requests the client does not echo.
        /// </summary>
        public void RequestEchoOff()
        {
            if (Flags.And(SuppressGoAhead))
                client.SendGoAhead();
            Flags |= SuppressEcho;
            client.SendWill(TelnetOption.Echo);
        }

        /// <summary>
        /// Requests the client echo's.
        /// </summary>
        public void RequestEchoOn()
        {
            if (Flags.And(SuppressGoAhead))
                client.SendGoAhead();
            Flags &= ~SuppressEcho;
            client.SendWont(TelnetOption.Echo);
        }

        private void ConfirmNewAccount(string argument)
        {
            if (string.IsNullOrEmpty(argument))
            {
                ConfirmNewAccount("Y");
                return;
            }
            switch (char.ToUpper(argument[0]))
            {
                default:
                    Write("Enter a password for " + account.Name + ": ");
                    SetState(GetNewPassword);
                    RequestEchoOff();
                    break;
                case 'N':
                    Write("Login: ");
                    account = null;
                    SetState(GetAccountName);
                    break;
            }
        }

        private void GetNewPassword(string argument)
        {
            if (string.IsNullOrEmpty(argument))
            {
                Write("Please enter a valid password: ");
                return;
            }

            account.Password = World.Encoding.GetBytes(argument);
            WriteLine();
            Write("Enter password again: ");
            SetState(ConfirmNewPassword);
        }

        private void ConfirmNewPassword(string argument)
        {
            if (!account.Password.ToArray().DeepCompare(World.Encoding.GetBytes(argument)))
            {
                if (++passwordCount > 3)
                {
                    WriteLine("Too many password attempts.  Disconnecting.");
                    log.Warn("Too many password attempts for " + account.Name);
                    client.Close();
                }
                else
                {
                    Write("Passwords do not match. Try again ({0}): ", passwordCount);
                }
                return;
            }
            WriteLine();
            Write("Please enter a valid email address: ");
            SetState(GetEmail);
            RequestEchoOn();
        }

        private void GetEmail(string argument)
        {
            if (!Util.IsEmail(argument))
            {
                Write("Please enter a valid email address: ");
                return;
            }

            account.Email = argument;
            World.DataContext.Accounts.InsertOnSubmit(account);
            AccountPlayerSelection();
            SetState(GetPlayer);
        }


        private void GetAccountPassword(string argument)
        {
            byte[] passwd = World.Encoding.GetBytes(argument);

            if (!account.Password.ToArray().DeepCompare(passwd))
            {
                if (++passwordCount > 3)
                {
                    WriteLine("Too many password attempts.  Disconnecting.");
                    log.Warn("Too many password attempts for " + account.Name);
                    client.Close();
                }
                else
                {
                    Write("Passwords do not match. Try again ({0}): ", passwordCount);
                }
                return;
            }
            RequestEchoOn();
            AccountPlayerSelection();
            SetState(GetPlayer);
        }

        private void AccountPlayerSelection()
        {
            WriteLine();
            WriteLine("C) Create New Character");
            for (int i = 0; i < account.Players.Count; i++)
            {
                WriteLine("{0}) {1}", i + 1, account.Players[i].Name);
            }
            Write("What character would you like to play? ");
        }

        private void GetPlayer(string argument)
        {
            if (string.IsNullOrEmpty(argument))
            {
                Write("Please select a valid option: ");
                return;
            }

            if (char.ToUpper(argument[0]) == 'C')
            {
                Write("What is your character's name? ");
                SetState(GetNewCharName);
                return;
            }
            if (!argument.IsNumber())
            {
                GetPlayer(null);
                return;
            }

            int option = int.Parse(argument) - 1;

            if (option < 0 || option > account.Players.Count)
            {
                GetPlayer(null);
                return;
            }

            Playing = account.Players[option];
            Playing.Connection = this;
            DisplayMotd();
            SetState(PlayingState);
        }

        private void GetNewCharName(string argument)
        {
            if (string.IsNullOrEmpty(argument) || argument.Length > 12)
            {
                Write("Please enter a valid character name (12 letters or less): ");
                return;
            }

            argument = argument.Capitalize();

            if (World.DataContext.Characters.OfType<Player>().Where(p => p.Name == argument).Count() > 0)
            {
                WriteLine("There is already a player with that name.");
                Write("Please enter another name: ");
                return;
            }

            Playing = new Player
                          {
                              Account = account,
                              Name = argument,
                              Connection = this,
                          };
            Write("Create a character named " + Playing.Name + "? (Y/n) ");
            SetState(ConfirmNewCharName);
        }

        private void ConfirmNewCharName(string argument)
        {
            if (string.IsNullOrEmpty(argument))
            {
                ConfirmNewCharName("Y");
                return;
            }

            switch (char.ToUpper(argument[0]))
            {
                default:
                    DisplayRaces();
                    Write("What is your race? ");
                    SetState(GetCharRace);
                    break;

                case 'N':
                    Write("Ok, what is your characters name then? ");
                    Playing = null;
                    break;
            }
        }

        private void DisplayRaces()
        {
            WriteLine();
            foreach (PlayerRace r in World.Races.OfType<PlayerRace>())
            {
                WriteLine("{0,-10}: {1}", r.Name, r.Description);
            }
        }

        private void GetCharRace(string argument)
        {
            PlayerRace r =
                World.Races.OfType<PlayerRace>().FindByName(argument);

            if (r == null)
            {
                Write("No such race! Try again: ");
                return;
            }
            Playing.Race = r;
            DisplayClasses();
            Write("What kind of character shall you be? ");
            SetState(GetCharClass);
        }

        private void DisplayClasses()
        {
            WriteLine();
            foreach (ClassType c in World.Classes)
            {
                WriteLine("{0,-10}: {1}", c.Name, c.Description);
            }
        }

        private void GetCharClass(string argument)
        {
            ClassType cl =
                World.Classes.FindByName(argument);

            if (cl == null)
            {
                Write("No such class! Try again: ");
                return;
            }
            Playing.Class = cl;
            account.Players.Add(Playing);
            World.DataContext.Characters.InsertOnSubmit(Playing);
            DisplayMotd();
            SetState(PlayingState);
        }

        private void DisplayMotd()
        {
            WriteLine("* Don't feed the immortals.");
            WriteLine();
            WriteLine("[Hit return to continue]");

            if (Playing.Level == 0)
            {
                if (World.DataContext.Characters.OfType<Player>().Count() == 0)
                {
                    Playing.Level = World.MAX_LEVEL;
                    log.Warn("Setting first player " + Playing.Name + " immortal");
                    Playing.WriteLine("You are now level " + Playing.Level + "!");
                }
            }
            if (Playing.Room == null)
                Playing.Room = World.DataContext.Rooms.FirstOrDefault();

            Playing.Room.Characters.Add(Playing);

            World.Connections.Add(this);
            World.Characters.Add(Playing);

            log.Info(Playing.Name + " has logged in");
        }


        private void SetState(StateHandler newState)
        {
            state = newState;
        }

        private delegate void StateHandler(string argument);

        #endregion

        public void Run()
        {
            RequestEchoOn();

            WriteLine("Welcome to " + World.Name + "!");

            Write("Account Login: ");
            SetState(GetAccountName);

            while (Server.Instance.State != ServerState.Stopped && client.Parser.Socket.Connected)
            {
                try
                {
                    string line = reader.ReadLine();
                    if (line == null)
                        break;
                    state.Invoke(line);
                }
                catch (IOException)
                {
                    break;
                }
            }
        }

        public void Close()
        {
            client.Close();
        }
    }
}